Vibe Audit_02_첫 개발과 테스트
GitHub: Vibe-Audit
파이프라인 구조
전체 흐름은 이렇다.
- CLI 훅이 대화 이벤트를 수집해서 백엔드로 보낸다
- 백엔드가 이벤트를 받으면 Baseline을 생성하거나 갱신한다
- 턴마다 Phase를 분류하고(continuation/new_phase/pivot)
- Snapshot을 찍어서 현재 상태를 기록한다
- Alignment를 체크해서 의도 정렬 점수를 매긴다
- 이상이 감지되면 Flag를 생성한다
- SSE로 프론트에 실시간 전달한다
DB 모델은 여섯 개 테이블로 잡았다. Session, Event, Baseline, Phase, Snapshot, Flag.
SQLAlchemy ORM으로 정의했고 SQLite에 저장한다. 서버리스로 가볍게 돌리려면 이게 낫다.
Baseline이 핵심이다. 첫 사용자 메시지가 들어오면 LLM으로 의도를 추출해서 goal, must_have, must_not, success_checks를 자동 생성한다.
이후 모든 분석이 이 Baseline을 기준선으로 삼는다.
Phase 분류는 사용자 메시지가 들어올 때마다 돌린다.
LLM에게 현재 goal과 must_have, 그리고 새 메시지의 context_messages를 넘기면
continuation인지 new_phase인지 pivot인지 판단하고 confidence 점수를 같이 돌려준다.
여기에 confidence 정책을 덧씌웠다. pivot인데 confidence가 낮으면 new_phase로 다운그레이드,
new_phase인데 confidence가 더 낮으면 continuation으로 내린다.
반대로 topic_relation_score가 0.2 이하거나 명시적으로 방향을 버렸다면 무조건 pivot으로 올린다.
pivot이 확정되면 기존 Baseline을 아카이브하고 새 Baseline을 생성한다. 버전이 올라간다.
Alignment 체크는 assistant_response가 올 때 실행된다. user_message에서는 Phase 분류만 하고
에이전트가 응답을 완료한 시점에 전체 분석(Snapshot+Alignment+Flag)을 돌린다.
이렇게 분리한 이유는 tool_use 이벤트마다 분석을 돌리면 한 턴에 스냅샷이 N개씩 쌓이고 거짓 양성이 늘기 때문이다.
시스템 경계가 진짜 문제
구현보다 더 곤란했던 건 시스템 경계 문제다.
Claude Code, Gemini CLI, Codex CLI는 비슷해 보여도 이벤트 구조와 턴 경계가 다 다르다.
같은 프롬프트를 넣어도 phase 분류나 정렬 점수가 다르게 나오는 원인이 여기서 시작됐다.
결국 각 CLI 훅에서 이벤트를 정규화하는 레이어를 따로 만들어야 했는데 이 작업이 제일 곤란했다.
Claude Code
Claude Code는 네 가지 훅 이벤트(UserPromptSubmit/PostToolUse/Stop/SubagentStop)를 쏴주는데 문제는 세션 ID가 안정적이지 않다는 점이었다.
롱 컨텍스트 압축이나 서브에이전트 전환 시 내부적으로 새 세션 ID가 발급되면서 같은 대화인데 세션이 쪼개지는 현상이 생겼다.
이걸 해결하려고 트랜스크립트 파일 경로를 SHA1으로 해싱해서 바인딩 키로 쓰는 구조를 만들었다.
새 세션 ID가 들어와도 같은 트랜스크립트에 속하면 기존 Vibe Audit 세션으로 앨리어싱한다.
Stop 이벤트에서는 트랜스크립트 JSONL 파일을 직접 파싱해서 어시스턴트 응답을 역순으로 추출한다. 훅 자체가 응답 본문을 넘겨주지 않기 때문이다.
Claude에서는 내부 <task-notification> 같은 제어성 메시지가 사용자 턴으로 잡히는 문제도 있었다.
이게 Baseline에 섞이면 의도 추출 자체가 오염된다. 훅 단에서 내부 프롬프트를 필터링하도록 처리했다.
Gemini CLI
Gemini CLI는 AfterModel 훅 하나로 모든 스트리밍 청크가 들어온다. 약 300ms 간격으로 쏟아지는데, 매 청크마다 백엔드를 호출하면 분석이 N번 도는 셈이니까 안 된다.
그래서 fast path와 full path를 분리했다. 청크가 올 때는 파일에 append만 하고(fast path, 락 없음), finishReason이 STOP인 마지막 청크가 오면 파일 락을 걸고 누적된 텍스트를 합쳐서 한 번에 보낸다(full path).
파일 락은 OS 레벨 atomic create(O_CREAT|O_EXCL)로 구현했다. 프로세스 간 동기화를 파일 시스템으로 처리한 건 좀 원시적이지만 외부 의존성 없이 돌아가야 했으니까.
Codex CLI
Codex CLI는 아예 훅 구조가 달랐다. config.toml에 notify = [...] 배열로 스크립트를 지정하는 방식인데 이게 TOML 최상위에 있어야 한다.
마지막 테이블 아래에 붙으면 해당 테이블 소속으로 파싱돼서 무시된다. 이것 때문에 삽입 위치를 첫 번째 [ 테이블 선언 앞으로 강제하는 로직을 따로 넣었다.
훅 설치, 혼자만 다른 Gemini CLI 등등
훅 설치도 골치였다. Claude는 ~/.claude/settings.json에 JSON으로 훅을 등록하고 Codex는 ~/.codex/config.toml에 TOML로 등록한다.
포맷이 다르니까 setup 스크립트에서 각각 파싱하고 기존 설정을 보존하면서 우리 훅만 삽입/업데이트/제거하는 로직을 만들었다.
Codex config에서는 관리 블록을 마커 주석(>>> vibe-audit codex notify >>>)으로 감싸서 업데이트 시 기존 블록만 교체하도록 했다.
Gemini CLI는 프로젝트 디렉토리의 .gemini/settings.json에 훅을 수동으로 걸어야 해서 전역 설치가 안 된다.
사용자 입장에서 "설치했는데 왜 안 돼?"가 발생할 수밖에 없는 구조다.
그래서 온보딩을 2단계로 나눴다. 1단계는 npx vibe-audit setup으로 훅 파일과 설정을 자동 생성하고, 2단계는 데몬을 띄워서 실제 수집이 동작하는지 확인한다.
문제가 생기면 요즘 유행처럼 다 붙어나오는 doctor를 쓰게 했다. npx vibe-audit doctor로 진단한다. Node.js 버전, Python 존재 여부, venv 상태, 훅 설치 여부, 데몬 서버 health check까지 한 번에 돌린다.
각 항목이 통과/실패로 찍히고 실패 시 구체적인 해결 방법을 제안한다.
다만 doctor에서 hook installed가 뜨더라도 실제 콜백이 불리는지는 디버그 로그로 최종 확인해야 한다. 설정 파일에 값이 있다는 것과 CLI가 실제로 호출한다는 건 별개 문제니까.
스트리밍 중에 Ctrl+C로 중단하면 마지막 턴 상태가 애매하게 남는 것도 문제였다.
턴 확정 전/후를 구분해서 저장하고 "수집은 됐지만 분석은 미완료" 상태를 따로 구분하도록 했다.
첫 테스트
프로토타입이 어느 정도 돌아가게 되니까 실제 세션으로 테스트해봤다.
내가 평소에 Claude Code로 작업하던 세션을 그대로 물려서 돌렸다.
Baseline이 첫 메시지에서 자동으로 생성되고 이후 턴마다 alignment score가 찍히는 걸 보니까 꽤 뿌듯했다.
실제로 맥락이 틀어지는 순간에 점수가 떨어지고 phase가 pivot으로 전환되는 게 확인됐다.
근거도 같이 나오니까 "왜 이 점수인지"를 바로 볼 수 있었다.
물론 완벽하진 않았다. LLM 해석이 들어가는 부분이라 경계 케이스에서 판단이 흔들리는 경우가 있었다.
같은 세션을 다시 분석해도 미묘하게 다른 결과가 나올 때도 있었다.
이건 LLM 기반 분석의 태생적 한계라고 봤는데, 규칙 기반 신호와 병행하는 Dual Flag 구조로 어느 정도 보완할 수 있다고 생각했다.
아무튼 핵심 파이프라인은 동작한다. 수집, 정규화, 분석, 표시까지 한 바퀴가 돈다.